Skip to content

feat(graphql-server): attribute a request to the API's entity, as a whole pair - #1775

Closed
pyramation wants to merge 1 commit into
mainfrom
feat/attribute-api-entity
Closed

feat(graphql-server): attribute a request to the API's entity, as a whole pair#1775
pyramation wants to merge 1 commit into
mainfrom
feat/attribute-api-entity

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

A GraphQL request already tells the database who is acting (jwt.claims.user_id) and which database it landed on, but never which entity the work belongs to. Downstream (constructive-db) now refuses to create work — jobs, invocations — that has neither an actor nor an entity, so any request that enqueues work through a surface without an authenticated user fails with ATTRIBUTION_REQUIRED, and even authenticated work is unattributable for billing. The server is the only place that can supply it: it owns route resolution, req.databaseId, and the request transaction.

So buildPreset now stamps the entity pair into the same shared context as the other provenance claims, before the authenticated/anonymous split, so both branches carry it:

if (apiEntityType && req.databaseId) {
  context['jwt.claims.entity_id']   = req.databaseId;
  context['jwt.claims.entity_type'] = apiEntityType;
}

Two constraints worth stating, because both are load-bearing:

  • The pair is indivisible. Either both claims are set or neither is. Half a pair is worse than none — a consumer that reads entity_id without a type has to guess the type, and a defaulted 'platform' is a lie for every other surface. Hence no fallback entity type and no set-one-without-the-other path.
  • The entity type is configuration, not process.env. It arrives as api.entityType through ApiOptions/getGraphQLEnvVars (API_ENTITY_TYPE), per AGENTS.md's rule that config is read through the env options system, not scraped at the point of use. Unset means the server stamps nothing and behaves exactly as before — this is opt-in per deployment, since only the deployment knows whether its surface is platform, database, or something else.

Tests cover the three cases that matter: both present → complete pair; entity type unset → no entity claims; databaseId missing → no entity claims.

Link to Devin session: https://app.devin.ai/sessions/47477486a4fd45e684bd663b7007a629
Requested by: @pyramation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Document API_ENTITY_TYPE in env README (README.md) — The new API_ENTITY_TYPE env var is parsed into api.entityType (graphql/env/src/env.ts:70) but is missing from the API Configuration section of graphql/env/README.md, where every sibling API_* var (API_ROUTING_SCHEMA, API_IS_PUBLIC, API_EXPOSED_SCHEMAS, API_META_SCHEMAS, API_ANON_ROLE, API_ROLE_NAME) is documented.

The change introduces API_ENTITY_TYPE, parsed into api.entityType in graphql/env/src/env.ts, and wires it through ApiOptions into the PostGraphile preset and finally into the request-context JWT claim builder in graphql/server/src/middleware/graphile.ts, where entity_id/entity_type claims are set alongside database_id only when both apiEntityType and req.databaseId are present. A new test file covers the attribution logic. The implementation is functionally correct for the happy path and consistently guarded.

Files Change
graphql/env/src/env.ts, graphql/env/tests/merge.test.ts Adds API_ENTITY_TYPE parsing and merges it into the API options object with tests.
graphql/server/src/middleware/graphile.ts Threads apiEntityType into the preset and request-context JWT claim attribution.
graphql/server/src/middleware/tests/graphile-entity-attribution.test.ts New test for entity claim attribution (re-implements the logic locally).
graphql/types/src/graphile.ts Adds the entityType field to the public API options interface.

Reviewed commit: 5d8c6ae

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds an API_ENTITY_TYPE env var that threads an entity type through the GraphQL env options, preset, and request-context JWT claim attribution, plus a new test for the attribution logic.

Key findings

Comment on lines +4 to +20
const buildContext = (
req: Partial<Request>,
entityType?: string
): Record<string, string> => {
const context: Record<string, string> = {};

if (entityType && req.databaseId) {
context['jwt.claims.entity_id'] = req.databaseId;
context['jwt.claims.entity_type'] = entityType;
}

if (req.databaseId) {
context['jwt.claims.database_id'] = req.databaseId;
}

return context;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Test duplicates logic instead of exercising middleware

The new test graphile-entity-attribution.test.ts re-implements the claim-building logic in a local buildContext helper (graphile-entity-attribution.test.ts:4-20) that mirrors graphile.ts:218-221 and asserts only against that copy, never importing or invoking the real buildPreset context builder. Because the test and production code are separate copies, a regression in the middleware's attribution logic (wrong predicate, dropped claim, mis-ordered override) would leave these tests green, so the suite provides no protection for the behavior it claims to verify.

📋 Prompt for AI Agents

In graphql/server/src/middleware/graphile.ts, extract the context-building block (currently the if (req) { ... } that sets jwt.claims.entity_id/entity_type/database_id/api_id etc., around lines 217-243) into an exported pure helper such as export function buildJwtClaimContext(req, apiEntityType): Record<string,string>, and call it from the grafast context callback. Then rewrite graphql/server/src/middleware/__tests__/graphile-entity-attribution.test.ts to import and assert against that exported helper (ideally plus an integration test that drives a request through the middleware) instead of re-implementing the same logic in a local buildContext, so the test exercises the real production code path and catches regressions in the attribution logic.

@pyramation pyramation closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant